/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import static android.persist.op.sqlite.SQLLogging.SQL_LOG_TAG;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.persist.PersistException;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;
import android.persist.op.OpException;
import android.persist.op.Update;
import android.reflect.util.ClassReflectionUtil;
import android.util.Log;

/**
 * {@code SQLiteUpdate updates an existing object in the SQLite database using a SQL "UPDATE" query.
 * @author Richard Schilling
 *
 * @param <OT> the type of class that will be updated by this operation.
 * @since 1.0
 */
public class SQLiteUpdate<OT> extends Update<OT, Integer> {

    /**
     * The database that this operation is applied against.
     * 
     * @since 1.0
     */
    private final SQLiteDatabase mDatabase;

    /**
     * Class configuration object for {@link operand}'s type.
     * 
     * @since 1.0
     */
    private final ClassConfig<OT> mConfig;

    /**
     * The values to update.
     * 
     * @since 1.0
     */
    private final Map<FieldConfig, Object> mValues;

    /**
     * The where key/values used to retrieve information about the operand
     */
    private final String mWhereValues;

    /**
     * Creates a new instance. If {@code values} is null, then it is ignored. If
     * it is not null then all fields in the object {@code o} are ignored - only
     * the fields in the map's key set will be saved in the database, and the
     * specified values in the map will be used. Passing in a values object is
     * the only way to update foreign references.
     * <p>
     * The keys in the object passed to the constructor are always used.
     * 
     * @param db the database to write to
     * @param o the object to read data from.
     * @param values an optional map of fields from o and the values that should
     *            be saved.
     * @throws IllegalArgumentException if db or o is null.
     * @throws IllegalArgumentException if values contains a {@code FieldConfig}
     *             that is one of {@code o}'s keys.
     * @throws OpException if {@code db} is read only.
     * @throws IllegalArgumentException if one of {@code o}'s keys are null.
     * @since 1.0
     */
    @SuppressWarnings("unchecked")
    public SQLiteUpdate(SQLiteDatabase db, OT o, Map<FieldConfig, Object> values) {
        super(o);
        if (db == null) {
            throw new IllegalArgumentException("db cannot be null.");
        }
        if (o == null) {
            throw new IllegalArgumentException("o cannot be null.");
        }

        if (db.isReadOnly()) {
            throw new OpException("database is read only");
        }

        this.mDatabase = db;
        this.mValues = values;
        this.mConfig = (ClassConfig<OT>) ClassConfig.getConfig((Class<OT>) operand.getClass());

        // make sure that no key is null.
        if (this.mConfig.getNullKeys(operand).size() > 0) {
            throw new IllegalArgumentException("o cannot have null values for keys");
        }

        /*
         * make sure that the names of keys in o are not found as a FieldConfig.
         */
        if (values != null) {

            List<String> keyNames = this.mConfig.getKeyNames();

            Set<FieldConfig> valuesFieldConfigs = values.keySet();
            for (FieldConfig fc : valuesFieldConfigs) {
                if (keyNames.contains(fc.getPersistedName())) {
                    throw new IllegalArgumentException("a FieldConfig that is used as a key in "
                            + this.mConfig.clazz.getName()
                            + " is not allowed in the parameter values.");
                }
            }
        }

        mWhereValues = SQLiteUtil.generateWhere(operand);

    }

    /**
     * Executes the UPDATE statement.
     * 
     * @return the number of rows affected by the update.
     * @since 1.0
     */
    @Override
    public Integer call() {

        ContentValues values = new ContentValues();
        Map<FieldConfig, Object> fieldValues = mValues == null ? mConfig
                .getDomesticFieldValues(operand)
                : mValues;
        for (Entry<FieldConfig, Object> entry : fieldValues.entrySet()) {
            SQLiteUtil.addContentValue(entry.getKey().getPersistedName(), entry.getValue(), values);
        }

        // update collections

        updateCollections();
        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    "UPDATE table " + mConfig.storageName + " with values " + values.toString());
        }
        return mDatabase.update(mConfig.storageName, values, mWhereValues, null);
    }

    @SuppressWarnings({
            "rawtypes", "unchecked"
    })
    private void updateCollections() {

        /*
         * Build up a list of FieldConfig objects to update
         */

        List<FieldConfig> updatedFieldConfigs = new ArrayList<FieldConfig>();

        if (mValues == null) {
            updatedFieldConfigs = mConfig.foreignFields;
        } else {
            for (FieldConfig fConfig : mValues.keySet()) {
                if (fConfig.isCollection()) {
                    updatedFieldConfigs.add(fConfig);
                }

            }
        }

        if (updatedFieldConfigs.size() == 0) {
            return;
        }

        StringBuilder idQuery = new StringBuilder();
        idQuery.append("SELECT ROWID FROM '");
        idQuery.append(mConfig.storageName);
        idQuery.append("' WHERE ");
        idQuery.append(mWhereValues);

        SQLiteStatement idStatement = mDatabase.compileStatement(idQuery.toString());
        long parentROWID = idStatement.simpleQueryForLong();

        for (FieldConfig fConfig : mConfig.foreignFields) {

            if (!fConfig.isCollection()) {
                continue;
            }

            StringBuilder childTableName = new StringBuilder();
            childTableName.append(mConfig.storageName);
            childTableName.append("_");
            childTableName.append(fConfig.getPersistedName());

            /*
             * delete the records in the collection table
             */
            StringBuilder childQuery = new StringBuilder();

            childQuery.append("DELETE FROM '");
            childQuery.append(childTableName.toString());
            childQuery.append("' WHERE parent_ROWID=");
            childQuery.append(Long.toString(parentROWID));

            if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                Log.d(SQL_LOG_TAG,
                        childQuery.toString());
            }
            mDatabase.execSQL(childQuery.toString());

            Class<?> columnTypeClass = fConfig.getCollectionType();

            ContentValues values = new ContentValues();

            try {
                Collection<?> collection = (Collection<?>) fConfig.field.get(operand);

                if (ClassReflectionUtil.isPrimitiveOrArray(columnTypeClass)) {
                    for (Object o : collection) {
                        values.put("parent_ROWID", parentROWID);
                        // save the value as-is.
                        SQLiteUtil.addContentValue(fConfig.getPersistedName(), o, values);

                        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                            Log.d(SQL_LOG_TAG,
                                    "INSERT table " + childTableName.toString() + " with values "
                                            + values.toString());
                        }
                        mDatabase.insertOrThrow(childTableName.toString(), null,
                                values);

                        values.clear();

                    }
                } else {

                    List<Long> foreignRowIds = new ArrayList<Long>();
                    for (Object o : collection) {
                        values.put("parent_ROWID", parentROWID);

                        /*
                         * save or update the foreign object.
                         */
                        Long rowId = new SQLiteGetRowId(mDatabase, o).call();

                        if (rowId != null) {
                            // update
                            SQLiteUpdate<?> updateOp = new SQLiteUpdate(mDatabase, o, null);
                            updateOp.call();

                        } else {
                            // create
                            SQLiteCreate<?> createOp = new SQLiteCreate(mDatabase, o);
                            rowId = createOp.call();
                        }

                        values.put(fConfig.getPersistedName(), rowId);

                        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                            Log.d(SQL_LOG_TAG,
                                    "INSERT table " + childTableName + " with values "
                                            + values.toString());
                        }
                        foreignRowIds.add(mDatabase.insertOrThrow(childTableName.toString(),
                                null,
                                values));

                        values.clear();
                    }

                    if (values.size() != 0) {
                        throw new IllegalStateException("values did not clear properly.");
                    }

                    /*
                     * in the child table save the row identifiers for each
                     * foreign object.
                     */
                    for (Long rowId : foreignRowIds) {
                        values.put("parent_ROWID", parentROWID);
                        SQLiteUtil.addContentValue(fConfig.getPersistedName(), rowId, values);

                        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                            Log.d(SQL_LOG_TAG,
                                    "INSERT table " + childTableName.toString() + " with values "
                                            + values.toString());
                        }
                        mDatabase.insertOrThrow(childTableName.toString(), null,
                                values);
                    }
                }
            } catch (IllegalArgumentException e) {
                throw new PersistException("unalbe to update foreign field " + fConfig.toString());
            } catch (IllegalAccessException e) {
                throw new PersistException("unalbe to update foreign field " + fConfig.toString());
            }

        }

    }
}
